"""
Audio mixer for combining multiple audio sources.

Supports mixing TTS audio with background music and sound effects.
"""

import os
import asyncio
from pathlib import Path
from typing import List, Dict, Optional, Any, Tuple
from dataclasses import dataclass
import tempfile

from ...utils.exceptions import MD2PPTError


@dataclass
class AudioTrack:
    """Represents an audio track with timing and properties."""
    file_path: str
    start_time: float = 0.0  # Start time in seconds
    duration: Optional[float] = None  # Duration in seconds (None = full length)
    volume: float = 1.0  # Volume multiplier (0.0 to 2.0)
    fade_in: float = 0.0  # Fade in duration in seconds
    fade_out: float = 0.0  # Fade out duration in seconds
    loop: bool = False  # Whether to loop the track
    track_type: str = "audio"  # audio, music, effect
    
    def __post_init__(self):
        """Validate track parameters."""
        if not os.path.exists(self.file_path):
            raise AudioMixerError(f"Audio file not found: {self.file_path}")
        
        if self.volume < 0.0 or self.volume > 2.0:
            raise AudioMixerError(f"Volume must be between 0.0 and 2.0, got {self.volume}")


@dataclass
class MixingConfig:
    """Configuration for audio mixing."""
    output_sample_rate: int = 44100
    output_channels: int = 2  # Stereo
    output_format: str = "mp3"
    output_bitrate: str = "192k"
    normalize_audio: bool = True
    master_volume: float = 1.0
    crossfade_duration: float = 0.1  # Crossfade between tracks
    
    def to_ffmpeg_args(self) -> List[str]:
        """Convert config to FFmpeg arguments."""
        args = [
            "-ar", str(self.output_sample_rate),
            "-ac", str(self.output_channels),
            "-b:a", self.output_bitrate
        ]
        
        if self.output_format == "mp3":
            args.extend(["-codec:a", "libmp3lame"])
        elif self.output_format == "wav":
            args.extend(["-codec:a", "pcm_s16le"])
        elif self.output_format == "aac":
            args.extend(["-codec:a", "aac"])
        
        return args


class AudioMixer:
    """Audio mixer using FFmpeg for professional audio processing."""
    
    def __init__(self, config: Optional[MixingConfig] = None):
        """Initialize audio mixer."""
        self.config = config or MixingConfig()
        self._temp_files: List[str] = []
    
    async def mix_tracks(self, tracks: List[AudioTrack], 
                        output_path: str,
                        total_duration: Optional[float] = None) -> str:
        """
        Mix multiple audio tracks into a single output file.
        
        Args:
            tracks: List of audio tracks to mix
            output_path: Output file path
            total_duration: Total duration of output (auto-calculated if None)
            
        Returns:
            Path to mixed audio file
        """
        if not tracks:
            raise AudioMixerError("No tracks provided for mixing")
        
        try:
            # Ensure FFmpeg is available
            await self._check_ffmpeg()
            
            # Calculate total duration if not provided
            if total_duration is None:
                total_duration = await self._calculate_total_duration(tracks)
            
            # Build FFmpeg command
            cmd = await self._build_ffmpeg_command(tracks, output_path, total_duration)
            
            # Execute FFmpeg
            process = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE
            )
            
            stdout, stderr = await process.communicate()
            
            if process.returncode != 0:
                error_msg = stderr.decode() if stderr else "Unknown FFmpeg error"
                raise AudioMixerError(f"FFmpeg mixing failed: {error_msg}")
            
            # Verify output file was created
            if not os.path.exists(output_path):
                raise AudioMixerError("Output file was not created")
            
            return output_path
            
        except Exception as e:
            # Clean up temporary files
            await self._cleanup_temp_files()
            if isinstance(e, AudioMixerError):
                raise
            else:
                raise AudioMixerError(f"Audio mixing failed: {e}")
    
    async def mix_narration_with_background(self, 
                                          narration_path: str,
                                          background_path: Optional[str] = None,
                                          output_path: Optional[str] = None,
                                          background_volume: float = 0.3) -> str:
        """
        Mix narration audio with background music.
        
        Args:
            narration_path: Path to narration audio
            background_path: Path to background music (optional)
            output_path: Output path (auto-generated if None)
            background_volume: Background music volume (0.0 to 1.0)
            
        Returns:
            Path to mixed audio file
        """
        if output_path is None:
            output_path = self._generate_temp_path("mixed_audio.mp3")
        
        tracks = [
            AudioTrack(
                file_path=narration_path,
                volume=1.0,
                track_type="narration"
            )
        ]
        
        if background_path and os.path.exists(background_path):
            # Get narration duration to loop background music
            narration_duration = await self._get_audio_duration(narration_path)
            
            tracks.append(AudioTrack(
                file_path=background_path,
                volume=background_volume,
                duration=narration_duration,
                loop=True,
                fade_in=1.0,
                fade_out=1.0,
                track_type="music"
            ))
        
        return await self.mix_tracks(tracks, output_path)
    
    async def create_slideshow_audio(self, 
                                   slide_audios: List[str],
                                   slide_durations: List[float],
                                   background_music: Optional[str] = None,
                                   output_path: Optional[str] = None) -> str:
        """
        Create audio for a complete slideshow.
        
        Args:
            slide_audios: List of audio files for each slide
            slide_durations: Duration for each slide
            background_music: Optional background music
            output_path: Output path
            
        Returns:
            Path to slideshow audio file
        """
        if len(slide_audios) != len(slide_durations):
            raise AudioMixerError("Number of audio files must match number of durations")
        
        if output_path is None:
            output_path = self._generate_temp_path("slideshow_audio.mp3")
        
        tracks = []
        current_time = 0.0
        
        # Add slide narrations
        for i, (audio_path, duration) in enumerate(zip(slide_audios, slide_durations)):
            if os.path.exists(audio_path):
                tracks.append(AudioTrack(
                    file_path=audio_path,
                    start_time=current_time,
                    volume=1.0,
                    track_type="narration"
                ))
            current_time += duration
        
        # Add background music if provided
        if background_music and os.path.exists(background_music):
            tracks.append(AudioTrack(
                file_path=background_music,
                start_time=0.0,
                duration=current_time,
                volume=0.2,
                loop=True,
                fade_in=2.0,
                fade_out=2.0,
                track_type="music"
            ))
        
        return await self.mix_tracks(tracks, output_path, total_duration=current_time)
    
    async def _build_ffmpeg_command(self, tracks: List[AudioTrack], 
                                  output_path: str, 
                                  total_duration: float) -> List[str]:
        """Build FFmpeg command for mixing tracks."""
        cmd = ["ffmpeg", "-y"]  # -y to overwrite output file
        
        # Add input files
        for track in tracks:
            cmd.extend(["-i", track.file_path])
        
        # Build filter complex for mixing
        filter_complex = await self._build_filter_complex(tracks, total_duration)
        cmd.extend(["-filter_complex", filter_complex])
        
        # Add output configuration
        cmd.extend(self.config.to_ffmpeg_args())
        
        # Set master volume
        if self.config.master_volume != 1.0:
            cmd.extend(["-filter:a", f"volume={self.config.master_volume}"])
        
        # Set output duration
        cmd.extend(["-t", str(total_duration)])
        
        # Output file
        cmd.append(output_path)
        
        return cmd
    
    async def _build_filter_complex(self, tracks: List[AudioTrack], 
                                  total_duration: float) -> str:
        """Build FFmpeg filter complex for mixing."""
        filters = []
        mix_inputs = []
        
        for i, track in enumerate(tracks):
            input_label = f"[{i}:a]"
            processed_label = f"[a{i}]"
            
            # Build processing chain for this track
            track_filters = []
            
            # Volume adjustment
            if track.volume != 1.0:
                track_filters.append(f"volume={track.volume}")
            
            # Fade in/out
            if track.fade_in > 0:
                track_filters.append(f"afade=t=in:ss=0:d={track.fade_in}")
            
            if track.fade_out > 0:
                fade_start = (track.duration or total_duration) - track.fade_out
                track_filters.append(f"afade=t=out:ss={fade_start}:d={track.fade_out}")
            
            # Loop if needed
            if track.loop and track.duration:
                track_filters.append(f"aloop=loop=-1:size={int(track.duration * self.config.output_sample_rate)}")
            
            # Delay for start time
            if track.start_time > 0:
                track_filters.append(f"adelay={int(track.start_time * 1000)}|{int(track.start_time * 1000)}")
            
            # Combine filters for this track
            if track_filters:
                filter_chain = ",".join(track_filters)
                filters.append(f"{input_label}{filter_chain}{processed_label}")
                mix_inputs.append(processed_label)
            else:
                mix_inputs.append(input_label)
        
        # Mix all processed tracks
        if len(mix_inputs) > 1:
            mix_filter = f"{''.join(mix_inputs)}amix=inputs={len(mix_inputs)}:duration=longest[out]"
            filters.append(mix_filter)
        else:
            # Single track, just rename
            filters.append(f"{mix_inputs[0]}acopy[out]")
        
        return ";".join(filters)
    
    async def _calculate_total_duration(self, tracks: List[AudioTrack]) -> float:
        """Calculate total duration needed for all tracks."""
        max_end_time = 0.0
        
        for track in tracks:
            if track.duration:
                end_time = track.start_time + track.duration
            else:
                # Get actual file duration
                file_duration = await self._get_audio_duration(track.file_path)
                end_time = track.start_time + file_duration
            
            max_end_time = max(max_end_time, end_time)
        
        return max_end_time
    
    async def _get_audio_duration(self, file_path: str) -> float:
        """Get duration of audio file using FFprobe."""
        cmd = [
            "ffprobe", "-v", "quiet", "-show_entries", "format=duration",
            "-of", "csv=p=0", file_path
        ]
        
        process = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE
        )
        
        stdout, stderr = await process.communicate()
        
        if process.returncode != 0:
            raise AudioMixerError(f"Failed to get audio duration: {stderr.decode()}")
        
        try:
            return float(stdout.decode().strip())
        except ValueError:
            raise AudioMixerError("Invalid duration returned by FFprobe")
    
    async def _check_ffmpeg(self):
        """Check if FFmpeg is available."""
        try:
            process = await asyncio.create_subprocess_exec(
                "ffmpeg", "-version",
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE
            )
            await process.communicate()
            
            if process.returncode != 0:
                raise AudioMixerError("FFmpeg is not working properly")
                
        except FileNotFoundError:
            raise AudioMixerError("FFmpeg not found. Please install FFmpeg.")
    
    def _generate_temp_path(self, filename: str) -> str:
        """Generate temporary file path."""
        temp_path = os.path.join(tempfile.gettempdir(), filename)
        self._temp_files.append(temp_path)
        return temp_path
    
    async def _cleanup_temp_files(self):
        """Clean up temporary files."""
        for temp_file in self._temp_files:
            try:
                if os.path.exists(temp_file):
                    os.remove(temp_file)
            except Exception:
                pass  # Ignore cleanup errors
        self._temp_files.clear()
    
    def __del__(self):
        """Cleanup when object is destroyed."""
        # Note: This won't work in async context, but provides fallback
        for temp_file in self._temp_files:
            try:
                if os.path.exists(temp_file):
                    os.remove(temp_file)
            except Exception:
                pass


class AudioMixerError(MD2PPTError):
    """Raised when audio mixing fails."""
    pass